Skip to content

fix(jvm): index Kotlin class properties + resolve same-name classes by import (#314)#539

Open
arttttt wants to merge 3 commits into
colbymchenry:mainfrom
arttttt:fix/kotlin-class-property-extraction
Open

fix(jvm): index Kotlin class properties + resolve same-name classes by import (#314)#539
arttttt wants to merge 3 commits into
colbymchenry:mainfrom
arttttt:fix/kotlin-class-property-extraction

Conversation

@arttttt
Copy link
Copy Markdown
Contributor

@arttttt arttttt commented May 28, 2026

Summary

Follow-up to #314 / #472. #472 fixed same-name class disambiguation for Java call sites (@Autowired FooConverter f; f.convert() resolves to the imported FooConverter, not whichever was found first). The equivalent Kotlin path was still broken — and the root cause turned out to be three stacked gaps, only the last of which #472 addressed.

Probed the actual graph to confirm (not guessed): on a synthetic two-module Kotlin repro, fooConverter.convert() with import com.example.b.FooConverter resolved to com.example.a — and there was no field node for fooConverter at all, so inferJavaFieldReceiverType returned null and #472's preferredFqn machinery never even ran for Kotlin.

The three gaps

  1. Kotlin class properties weren't indexed. extractField handled Java variable_declarator, C# variable_declaration → variable_declarator, and PHP property_element — but Kotlin property_declaration parses as variable_declaration → simple_identifier (+ user_type), matching none of them (and the identifier-only fallback misses it too, since Kotlin's grammar has no field names). Result: zero field nodes for Kotlin classes, so a val x: Foo receiver's declared type was invisible to the resolver.
  2. Kotlin imports weren't parsed. extractJavaImports required a trailing ;, which Java mandates but Kotlin omits (import com.example.Foo). So Kotlin produced zero import mappings and the receiver-FQN disambiguation signal never fired.
  3. Disambiguation assumed file-name = class-name. Even with type + import known, resolveMethodOnType matched candidates by file-path suffix (com/example/b/FooConverter.kt) — fine for Java, but Kotlin commonly puts FooConverter in Converters.kt, so the suffix never matched. Now it prefers the candidate whose namespace-qualified name (com.example.b::FooConverter, from feat(jvm): resolve Java/Kotlin imports by fully-qualified name #412's package-directive wrapper) matches the import, falling back to the file-path suffix for Java's layout.

Validation

Synthetic repro (the disambiguation proof): two modules each declaring class FooConverter in Converters.kt. Swapping only the caller's import line swaps the resolved target — import …a.FooConvertera::FooConverter::convert, import …b.FooConverterb::FooConverter::convert. Import-driven, not lexical-order-driven. Before the fix both resolved to a.

Real repos (nickbutcher/plaid 258 .kt, android/nowinandroid 310 .kt), indexed on main vs this branch:

plaid main→fix nia main→fix
Kotlin field nodes 0 → 485 0 → 329
Kotlin methods 753 = 753 772 = 772
Kotlin→Kotlin calls edges 1118 → 1162 1110 → 1159

The field-extraction fix is decisively validated — hundreds of previously-missing field nodes, with no node/edge explosion (methods stable, +~4% call edges, not multiples). On nia the +49 edges include call sites now correctly resolving to their test doubles (NewsResourceDao::getNewsResourcesTestNewsResourceDao::getNewsResources, UserDataRepository::…FakeUserDataRepository::…) — a real precision gain that fell out of seeing the field's declared type.

Honest scope note: the import-FQN disambiguation (gap 3) is proven on the synthetic repro. Neither real repo contained a true same-name-class-across-packages collision reached through the import + field-receiver shape, so its real-world impact is unmeasured there — it's correct-by-construction and showed zero regressions (no spurious target changes), but I'm not claiming a measured win for it on these two repos. Gaps 1 & 2 are the real-repo-proven part.

Test plan

  • New extraction test: Kotlin property_declarationfield node with "<Type> <name>" signature (incl. nullable Foo?Foo).
  • New resolution test: Kotlin same-name classes in non-eponymous files (Converters.kt) — call resolves to the imported package's class, not the lexically-first one.
  • Full suite green (tsc --noEmit clean; one pre-existing mcp-roots tinypool/Node-24 worker-crash flake, passes in isolation).
  • Real-repo before/after on plaid + nowinandroid (numbers above).

🤖 Generated with Claude Code

arttttt and others added 3 commits May 29, 2026 22:46
Kotlin `property_declaration` inside a class parses as
`variable_declaration → simple_identifier (+ user_type)`, which matches
neither the `variable_declarator` shape Java uses, the C#
`variable_declaration → variable_declarator` wrapper, nor PHP's
`property_element` — and the identifier-only fallback misses it too
(Kotlin's grammar has no field names). The result: Kotlin class
properties produced NO node at all, so a `val x: Foo` receiver's declared
type was invisible to the resolver and `x.method()` calls couldn't be
typed.

Add a Kotlin branch to extractField that reads the name from the
`simple_identifier` and the type from its sibling, emitting a
`"<Type> <name>"` signature (matching the Java/C# field shape that
inferJavaFieldReceiverType already reads). Kotlin nullable `Foo?` is
normalized to `Foo` so it resolves like the non-null type.

Groundwork for same-name class disambiguation on Kotlin call sites (colbymchenry#314).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
extractJavaImports required a trailing `;`, which Java mandates but Kotlin
omits (`import com.example.Foo`). So Kotlin produced ZERO import mappings
and the receiver-FQN disambiguation signal (the `importedFqn` passed to
resolveMethodOnType) never fired — same-name classes fell back to lexical
order. Make the `;` optional; Java's mandatory semicolon still matches
unchanged, and a Kotlin `as Alias` suffix still captures the FQN (alias
unhandled, as in the existing Java path). Part of colbymchenry#314.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…chenry#314)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@arttttt arttttt force-pushed the fix/kotlin-class-property-extraction branch from 5478eb1 to 5b4aba4 Compare May 29, 2026 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant